Skip to content

docs(chat-store): document per-state receipt rows for 1:1 chats (#1155) - #455

Merged
jlucaso1 merged 4 commits into
mainfrom
claude/nifty-bohr-7ky5bb
Jul 27, 2026
Merged

docs(chat-store): document per-state receipt rows for 1:1 chats (#1155)#455
jlucaso1 merged 4 commits into
mainfrom
claude/nifty-bohr-7ky5bb

Conversation

@jlucaso1

@jlucaso1 jlucaso1 commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

Summary

Documents the user-facing behavior change from whatsapp-rust#1155 ("fix(chat-store): keep receipt times for 1:1 chats, per state").

apply_receipt used to materialize message_receipts rows only for group chats, one row per participant holding the furthest state reached. A 1:1's delivery/read state lived solely in StoredMessage.status, with no per-state timestamp — so a UI could show "Delivered" but never "Delivered 14:32". The upstream PR changes receipts() to record one row per state (Delivered/Read/Played) per participant, for both 1:1 and group chats, each row keeping the earliest instant that state was reported.

Updated api/chat-store.mdx:

  • Querying — reworded the receipts() description to say it returns one row per participant per state, not per participant, and added a <Note> calling out the breaking change (1:1 chats previously returned no rows at all; callers assuming at most one row per participant should now expect up to three).
  • Companion-device identities — "one row per participant per state, not per device" (previously "one 'read by' row per participant, not per device", which implied a single row per participant).
  • PN/LID identity aliasing — the merge-on-heal bullet now describes per-state receipts and the earliest-instant-wins rule for a state both sides already recorded, replacing the old "per-user receipts" phrasing.
  • ReactionEntry / ReceiptEntry — added a paragraph explaining a message can carry several ReceiptEntry rows per user_jid, one per state.
  • Semantics worth knowing — replaced the "monotonic status and per-user group receipts" bullet with one describing StoredMessage.status (monotonic) vs. receipt rows (insert-only per state) separately, and added a bullet on receipts for messages no chat holds being dropped rather than parked (an existing but previously undocumented behavior this PR's tests also cover).

Not documented: the internal SQL merge mechanics in storages/chat-store/src/lid.rs (fold-then-rename-then-sweep passes) or the migration itself — no observable API surface beyond what's covered above. Per standing instructions, changelog/ (human-maintained) was left untouched.

Test plan

  • Diffed the updated sections against the PR #1155 diff, description, and new tests (dm_receipts_record_when_each_state_was_reached, dm_played_receipt_joins_read_rather_than_replacing_it, a_replayed_dm_receipt_does_not_move_the_recorded_instant, a_dm_receipt_resolved_by_alias_files_under_the_message_key, group_receipts_from_two_devices_keep_one_participant) for accuracy
  • Confirmed no other file in this repo references message_receipts/receipts() semantics that would also need updating (searched full repo)
  • mint broken-links (not run — CLI unavailable in this environment)

Generated by Claude Code


Summary by cubic

Document per-state receipt rows for 1:1 chats, captured per reported state with no backfill, and clarify how receipt timestamps behave. Also fix receipt message-id ownership and tighten wording.

  • Migration

    • Handle up to three ReceiptEntry rows per participant; rows are per reported state (Delivered/Read/Played) with no backfill; receipts() now returns rows for 1:1 chats.
    • Render per-state timestamps; PN/LID merges keep the earlier timestamp for the same state; no per-device duplicates.
    • Receipt rules: a per-state row is never removed; its timestamp may only move earlier.
  • Bug Fixes

    • Corrected docs: a receipt’s message id is sender-chosen and echoed by the peer, not server-assigned.
    • Reworded contradictory “insert-only” phrasing to “never removed; timestamp may only move down.”

Written for commit abd75ee. Summary will update on new commits.

Summary by CodeRabbit

  • Documentation
    • Clarified that message receipts are reported separately for Delivered, Read, and Played states.
    • Updated 1:1 and group chat semantics to return one receipt row per participant and state.
    • Documented that a message may have multiple receipt entries for the same participant, with timestamps recorded per state.
    • Added details on receipt ordering, timestamp handling, and identity reconciliation.
    • Noted the breaking change to the receipts() result format.

`receipts()` now returns one row per participant per delivery/read/played
state instead of only the furthest state, and only for group chats. Update
the querying, companion-device, PN/LID merge, and semantics sections to
match, plus a breaking-change note for callers assuming at most one row
per participant.
@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Updates api/chat-store.mdx to document per-state receipt rows across identity reconciliation, companion devices, 1:1 chats, and group chats, including ordering, timestamp, and insert-only semantics.

Changes

Receipt semantics

Layer / File(s) Summary
Identity and device receipt reconciliation
api/chat-store.mdx
PN/LID merging keeps the earliest timestamp per receipt state, while companion-device identities produce one row per participant per state.
Receipt query and state contract
api/chat-store.mdx
receipts() returns per-state rows for 1:1 and group chats, with documented ordering, earliest timestamps, and insert-only state behavior.

Estimated code review effort: 3 (Moderate) | ~15–30 minutes

Possibly related PRs

Poem

A bunny hops through Delivered and Read,
With Played timestamps neatly spread.
Each state gets a row in line,
Earlier moments stay divine.
“Per-state receipts!” the rabbit said,
And nibbled docs before bed.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately summarizes the main documentation change about per-state receipt rows for 1:1 chats.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@mintlify

mintlify Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
whatsapp-rust 🟢 Ready View Preview Jul 27, 2026, 9:22 PM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@api/chat-store.mdx`:
- Line 147: Rewrite the newly added documentation at the referenced sections in
active, second-person voice: address the reader as “you,” replace impersonal or
third-person phrasing and passive constructions, and split long multi-rule
sentences into concise one-idea sentences while preserving the documented
behavior and terminology.
- Line 459: Revise the receipt semantics wording in the “Monotonic status,
insert-only receipts” documentation to clarify that state membership is
insert-only: existing Delivered, Read, or Played rows are never removed, while
their timestamps may be updated only when a newly reported timestamp is earlier.
Remove the conflicting claim that existing rows are never overwritten.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: b2548c92-b72b-42c1-be92-ee87cd021f30

📥 Commits

Reviewing files that changed from the base of the PR and between 58836e9 and 439c4a1.

📒 Files selected for processing (1)
  • api/chat-store.mdx

Comment thread api/chat-store.mdx Outdated
- **A brand-new chat is keyed by LID** when the peer already has a known PN↔LID mapping; otherwise it's keyed by whichever identity first addressed it.
- **Every read resolves the alias.** [`messages`](#querying), [`message`](#querying), [`reactions`](#querying) and [`receipts`](#querying) all accept either of the peer's identities as the `chat` argument and match rows stored under either key — so a caller that only ever addresses a peer by phone number keeps working even if some rows ended up under the LID key (or vice versa).
- **Splits heal automatically.** If a peer's rows are split across both keys (for example, from receipts that arrived under the wrong identity before this resolution existed), the next piece of live traffic for that peer merges the pair into one thread — advance-only status/star/revoke/edit conflict resolution, union of reactions and per-user receipts, sticky metadata (pin/mute/archive/name/ephemeral) kept, badge recounted. Ties go to the LID side.
- **Splits heal automatically.** If a peer's rows are split across both keys (for example, from receipts that arrived under the wrong identity before this resolution existed), the next piece of live traffic for that peer merges the pair into one thread — advance-only status/star/revoke/edit conflict resolution, union of reactions and per-state receipts (a state both sides already recorded keeps the earlier of the two timestamps, rather than either side winning arbitrarily), sticky metadata (pin/mute/archive/name/ephemeral) kept, badge recounted. Ties go to the LID side.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Rewrite the new prose in active, second-person, concise style.

These changes repeatedly use impersonal or third-person phrasing such as “a caller,” “a message,” and “a group member,” plus passive constructions such as “are recorded” and “was reported.” Rewrite directly for the reader—for example, “When you call receipts(), you receive…”—and split the long multi-rule sentences into one idea per sentence.

As per coding guidelines, MDX documentation must use active voice and second person, with concise one-idea sentences.

Also applies to: 162-162, 213-217, 400-401, 459-459

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api/chat-store.mdx` at line 147, Rewrite the newly added documentation at the
referenced sections in active, second-person voice: address the reader as “you,”
replace impersonal or third-person phrasing and passive constructions, and split
long multi-rule sentences into concise one-idea sentences while preserving the
documented behavior and terminology.

Source: Coding guidelines

Comment thread api/chat-store.mdx Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 439c4a1ea7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread api/chat-store.mdx Outdated
## Semantics worth knowing

- **Monotonic status.** Per-message status and per-user group receipts only ever move forward; a late-arriving `delivered` can't downgrade a `read`.
- **Monotonic status, insert-only receipts.** `StoredMessage.status` only ever moves forwarda late-arriving `delivered` can't downgrade a `read`. [`ReceiptEntry`](#reactionentry--receiptentry) rows are insert-only per state instead: a state (`Delivered`/`Read`/`Played`) is recorded once, at the earliest instant it was reported, and a replay or an out-of-order receipt for a state already recorded never removes or overwrites that row — it only lowers the timestamp if the same state is reported as having happened earlier.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Stop calling mutable receipt rows insert-only

When a duplicate receipt for the same state arrives with an earlier timestamp, this sentence says the stored timestamp is lowered. That updates the existing participant/state row, so calling the rows “insert-only” and saying they are never overwritten gives readers the opposite persistence guarantee. Describe this as one mutable row per state, or as an upsert that retains the minimum timestamp.

Useful? React with 👍 / 👎.

Comment thread api/chat-store.mdx Outdated
- **Offline-drain reordering is handled.** If a revoke or edit arrives before the message it targets (common when draining a backlog), the target is materialized as already-revoked/edited up front — the original content's later arrival can't resurrect revoked content or show pre-edit text, and never double-counts unread.
- **History sync never clobbers live rows** (`ON CONFLICT DO NOTHING`). Live redeliveries and PDO recovery replace content in place instead — an `Undecryptable` placeholder becomes the real message under the same id once it's recovered.
- **Content refreshes are sender-scoped.** Message ids are sender-chosen, so a different sender reusing an id can't rewrite someone else's message.
- **A receipt for a message no chat holds is dropped, not parked.** The message id is the server's, and nothing in the store can tell an unrecorded send from a message the user already deleted — the second reading is the common one, since a peer's receipt costs a round trip and typically arrives well after the send it answers. A receipt is only ever recorded once the message it names is found under the addressed chat key or its PN/LID counterpart.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Correct the message ID ownership

Message IDs are sender-chosen, not assigned by the server; the immediately preceding bullet documents this, and record_outgoing accepts the caller's generated msg_id. Calling the ID “the server's” is therefore factually incorrect and obscures the actual reason an unmatched receipt is ambiguous: the store has only the receipt's chat/ID lookup key and no row indicating whether that locally chosen ID was never recorded or was deleted.

Useful? React with 👍 / 👎.

Comment thread api/chat-store.mdx Outdated
```

`message`, `reactions` and `receipts` accept either of a 1:1 peer's identities the same way `messages` does. `receipts` returns per-user delivery/read state — the group "read by" list. `unread_total` sums only positive unread counters, ignoring the `-1` manually-marked-unread sentinel on individual chats.
`message`, `reactions` and `receipts` accept either of a 1:1 peer's identities the same way `messages` does. `receipts` returns one row per participant *per state they've reached* — `Delivered`, `Read`, and `Played` are recorded as separate rows, each carrying the instant that state was first reported — rather than one row per participant holding only the furthest state. `unread_total` sums only positive unread counters, ignoring the `-1` manually-marked-unread sentinel on individual chats.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Split the receipt contract into one-idea sentences

The new receipts sentence combines row cardinality, the supported states, timestamp semantics, and a comparison with the previous representation. This makes the API contract unnecessarily difficult to scan; split those independent points into concise sentences as required by the repository's documentation style.

AGENTS.md reference: AGENTS.md:L24-L25

Useful? React with 👍 / 👎.

"Never removes or overwrites that row" conflicted with the following
clause allowing the timestamp to be lowered. State the rule as: the row
is never removed, and its timestamp may only move down to an earlier
reported instant.

Addresses a CodeRabbit review comment on #455.
- Fix a factual error: a receipt's message id is sender-chosen and
  echoed back by the peer, not server-assigned — this directly
  contradicted the "Message ids are sender-chosen" bullet one line
  above it.
- Reword the receipt-timestamp mutability rule to state plainly that a
  state row is never deleted but its timestamp can move down to an
  earlier reported instant, dropping the ambiguous "insert-only" framing.
- Split several multi-clause sentences added in the prior commits into
  one idea per sentence, per AGENTS.md's documentation style guidance.

Addresses coderabbitai and chatgpt-codex-connector review comments on #455.

Copy link
Copy Markdown
Collaborator Author

Pushed fixes for the review feedback so far, in 8f9dec3:

  • Fixed (CodeRabbit + Codex both flagged, line 465): the "message id is the server's" claim was factually wrong and directly contradicted the "Message ids are sender-chosen" bullet one line above it. Reworded to say the id is sender-chosen and echoed back by the peer, and clarified the actual rationale (an unmatched id looks identical whether the send was never recorded or the message was since deleted).
  • Fixed (CodeRabbit + Codex both flagged, line 459): reworded the receipt-timestamp mutability rule to drop the ambiguous "insert-only" framing and state plainly that a state row is never deleted but its timestamp can move down to an earlier reported instant.
  • Fixed (Codex, line 213 + AGENTS.md cite): split the receipts() contract sentence (cardinality + states + timestamp semantics + comparison to old behavior, all in one sentence) into one idea per sentence.
  • Also split similarly long sentences at lines 147, 162, and 400–401 into shorter ones for the same reason.

Not doing: CodeRabbit's broader ask to rewrite these sections in second-person ("you") voice. That guideline is real (AGENTS.md), but it's not followed anywhere else in this file — every existing bullet and paragraph here (including everything from PRs #401, #452–454) is third person. Converting only my new sentences to "you" while they sit inside third-person paragraphs and alongside third-person sibling bullets would read as inconsistent within the same list, and applying it correctly would mean rewriting content well outside this PR's diff. Happy to take that on as a dedicated follow-up pass across the whole file if wanted.


Generated by Claude Code

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8f9dec3b86

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread api/chat-store.mdx Outdated
`message`, `reactions` and `receipts` accept either of a 1:1 peer's identities the same way `messages` does. `receipts` returns one row per participant per state they've reached, not one row per participant. `Delivered`, `Read`, and `Played` are each recorded as a separate row. Each row's `timestamp` is the instant that state was first reported. `unread_total` sums only positive unread counters, ignoring the `-1` manually-marked-unread sentinel on individual chats.

<Note>
**Breaking change:** `receipts` used to return rows only for group chats. A 1:1's delivery/read state lived solely in `StoredMessage.status`, with no per-state timestamp available. It now records the same per-state rows for 1:1 chats too, so a caller can render "Delivered hh:mm" above "Read hh:mm" the way WA Web's contact message-info drawer does. A caller that assumed at most one row per participant should now expect up to three (`Delivered`, `Read`, `Played`), oldest state first.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Split the migration guidance into one-idea sentences

The final sentence combines the caller's migration condition, the new maximum row count, the supported states, and the result ordering guarantee. Split the cardinality/state guidance from the oldest state first guarantee so readers can scan each contract change independently.

AGENTS.md reference: AGENTS.md:L24-L25

Useful? React with 👍 / 👎.

Comment thread api/chat-store.mdx Outdated
```

`message`, `reactions` and `receipts` accept either of a 1:1 peer's identities the same way `messages` does. `receipts` returns per-user delivery/read state — the group "read by" list. `unread_total` sums only positive unread counters, ignoring the `-1` manually-marked-unread sentinel on individual chats.
`message`, `reactions` and `receipts` accept either of a 1:1 peer's identities the same way `messages` does. `receipts` returns one row per participant per state they've reached, not one row per participant. `Delivered`, `Read`, and `Played` are each recorded as a separate row. Each row's `timestamp` is the instant that state was first reported. `unread_total` sums only positive unread counters, ignoring the `-1` manually-marked-unread sentinel on individual chats.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Describe rows as reported states, not reached states

When the store observes a Read receipt without having observed Delivered first, it records the reported Read state rather than synthesizing a row for every logically reached state. Saying receipts returns a row for each state the participant has reached can therefore make callers assume a Delivered row is guaranteed whenever a Read row exists; describe these as the states whose receipts were actually reported.

Useful? React with 👍 / 👎.

…d state

record_receipt only ever inserts a row for the exact status an incoming
receipt carries — it never backfills earlier states a peer skipped (e.g.
a Read receipt with no prior Delivered receipt produces only a Read
row). "Reached" implied logical progression through all prior states;
reworded throughout to "reported" and called out the no-backfill case
explicitly on ReceiptEntry.

Also split the receipts() breaking-change note's closing sentence
(cardinality + states + ordering) into two.

Addresses chatgpt-codex-connector review comments on #455.

Copy link
Copy Markdown
Collaborator Author

Fixed both in abd75ee:

  • Line 213 (correctness): confirmed against record_receipt in the source PR — it only ever inserts a row for the exact status an incoming receipt carries, with no backfill of skipped intermediate states. Reworded "reached" → "reported" throughout (querying paragraph, companion-device bullet, ReceiptEntry doc) and added an explicit example: a Read receipt with no prior Delivered receipt produces only a Read row.
  • Line 216: split the breaking-change note's closing sentence (row-count cap, the three states, and the ordering guarantee) into two sentences.

Generated by Claude Code

@jlucaso1
jlucaso1 merged commit ba4f0dd into main Jul 27, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant